Completed
Push — master ( ae7fb3...9ade78 )
by Propa
9s
created

PhoneValidator::checkCountryField()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 1
Bugs 1 Features 1
Metric Value
c 1
b 1
f 1
dl 0
loc 12
ccs 4
cts 5
cp 0.8
rs 9.4285
cc 3
eloc 6
nc 3
nop 2
crap 3.072
1
<?php namespace Propaganistas\LaravelPhone;
2
3
use libphonenumber\NumberParseException;
4
use libphonenumber\PhoneNumberType;
5
use libphonenumber\PhoneNumberUtil;
6
use Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException;
7
use Propaganistas\LaravelPhone\Exceptions\InvalidParameterException;
8
9
class PhoneValidator
10
{
11
12
    /**
13
     * @var \libphonenumber\PhoneNumberUtil
14
     */
15
    protected $lib;
16
17
    /**
18
     * Whether the country should be auto-detected.
19
     *
20
     * @var bool
21
     */
22
    protected $autodetect = false;
23
24
    /**
25
     * Whether to allow lenient checking of numbers (i.e. landline without area codes).
26
     *
27
     * @var bool
28
     */
29
    protected $lenient = false;
30
31
    /**
32
     * The field to use for country if not passed as a parameter.
33
     *
34
     * @var null|string
35
     */
36
    protected $countryField = null;
37
38
    /**
39
     * Countries to validate against.
40
     *
41
     * @var array
42
     */
43
    protected $countries = [];
44
45
    /**
46
     * Transformed phone number types to validate against.
47
     *
48 18
     * @var array
49
     */
50 18
    protected $types = [];
51 16
52
    /**
53
     * PhoneValidator constructor.
54
     */
55
    public function __construct(PhoneNumberUtil $lib)
56
    {
57
        $this->lib = $lib;
58
    }
59
60
    /**
61
     * Validates a phone number.
62
     *
63
     * @param  string $attribute
64 18
     * @param  mixed  $value
65
     * @param  array  $parameters
66 16
     * @param  object $validator
67
     * @return bool
68 15
     * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
69
     * @throws \Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException
70
     */
71
    public function validatePhone($attribute, $value, array $parameters, $validator)
72 12
    {
73 4
        $countryField = $this->checkCountryField($parameters, $validator);
74 4
75
        $filterCountryFieldCallback = function ($value) use ($countryField) {
76 12
            return $value !== $countryField;
77 12
        };
78 12
79
        $this->assignParameters(array_map('strtoupper', array_filter($parameters, $filterCountryFieldCallback)));
80 12
81
        $this->checkCountries($attribute, $validator);
82
83 12
        // If autodetecting, let's first try without a country.
84 2
        // Otherwise use provided countries as default.
85
        if ($this->autodetect || ($this->lenient && empty($this->countries))) {
86
            array_unshift($this->countries, null);
87
        }
88
89
        foreach ($this->countries as $country) {
90
            if ($this->isValidNumber($value, $country)) {
91
                return true;
92 18
            }
93
        }
94 18
95
        // All specified country validations have failed.
96 16
        return false;
97 12
    }
98 11
99 12
    /**
100 7
     * Parses parameters to find one that matches a field for country input.
101 11
     *
102 2
     * @param array $parameters
103 7
     * @param object $validator
104 2
     *
105 2
     * @return null|string
106
     */
107 2
    public function checkCountryField(array $parameters, $validator)
108
    {
109 17
        $data = array_dot($validator->getData());
110
111 14
        foreach ($parameters as $parameter) {
112
            if (isset($data[$parameter])) {
113 14
                return $this->countryField = $parameter;
114
            }
115
        }
116
117
        return null;
118
    }
119
120
    /**
121
     * Parses the supplied validator parameters.
122
     *
123
     * @param array $parameters
124 15
     * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
125
     */
126 14
    protected function assignParameters(array $parameters)
127 15
    {
128
        $types = array();
129 15
130 6
        foreach ($parameters as $parameter) {
131 15
            if ($this->isPhoneCountry($parameter)) {
132 2
                $this->countries[] = $parameter;
133
            } elseif ($this->isPhoneType($parameter)) {
134 12
                $types[] = $parameter;
135
            } elseif ($parameter == 'AUTO') {
136
                $this->autodetect = true;
137
            } elseif ($parameter == 'LENIENT') {
138
                $this->lenient = true;
139
            } else {
140
                // Force developers to write proper code.
141
                throw new InvalidParameterException($parameter);
142
            }
143 12
        }
144
145
        $this->types = $this->parseTypes($types);
146
147
    }
148 12
149
    /**
150
     * Checks the detected countries. Overrides countries if a country field is present.
151 12
     * When using a country field, we should validate to false if country is empty so no exception
152
     * will be thrown.
153
     *
154 12
     * @param string $attribute
155 2
     * @param $validator
156
     * @throws \Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException
157
     */
158
    protected function checkCountries($attribute, $validator)
159 12
    {
160
        $data = array_dot($validator->getData());
161
        $countryField = ($this->countryField == false ? $attribute . '_country' : $this->countryField);
162 12
163
        if (isset($data[$countryField])) {
164 2
            $this->countries = array($data[$countryField]);
165
        } elseif (!$this->autodetect && !$this->lenient && empty($this->countries)) {
166
            throw new NoValidCountryFoundException;
167
        }
168 10
    }
169
170
    /**
171
     * Performs the actual validation of the phone number.
172 8
     *
173
     * @param mixed $number
174
     * @param null  $country
175
     * @return bool
176 8
     */
177
    protected function isValidNumber($number, $country = null)
178
    {
179
        try {
180
            // Throws NumberParseException if not parsed correctly against the supplied country.
181
            // If no country was given, tries to discover the country code from the number itself.
182
            $phoneNumber = $this->lib->parse($number, $country);
183
184
            // Check if type is allowed.
185
            if (empty($this->types) || in_array($this->lib->getNumberType($phoneNumber), $this->types)) {
186
187
                // Lenient validation.
188 14
                if ($this->lenient) {
189 4
                    return $this->lib->isPossibleNumber($phoneNumber, $country);
190 14
                }
191
192
                // For automatic detection, the number should have a country code.
193 14
                if ($phoneNumber->hasCountryCode()) {
194 4
195 4
                    // Automatic detection:
196
                    if ($this->autodetect) {
197 15
                        // Validate if the international phone number is valid for its contained country.
198 1
                        return $this->lib->isValidNumber($phoneNumber);
199
                    }
200
201
                    // Validate number against the specified country.
202
                    return $this->lib->isValidNumberForRegion($phoneNumber, $country);
203
                }
204
            }
205
206
        } catch (NumberParseException $e) {
207 12
            // Proceed to default validation error.
208
        }
209 12
210
        return false;
211
    }
212
213
    /**
214
     * Parses the supplied phone number types.
215
     *
216
     * @param array $types
217
     * @return array
218 10
     */
219
    protected function parseTypes(array $types)
220
    {
221 10
        // Transform types to their namespaced class constant.
222
        array_walk($types, function(&$type) {
223 10
            $type = constant('\libphonenumber\PhoneNumberType::' . $type);
224
        });
225
226
        // Add in the unsure number type if applicable.
227
        if (array_intersect([PhoneNumberType::FIXED_LINE, PhoneNumberType::MOBILE], $types)) {
228
            $types[] = PhoneNumberType::FIXED_LINE_OR_MOBILE;
229
        }
230
231
        return $types;
232
    }
233
234
    /**
235
     * Checks if the supplied string is a valid country code using some arbitrary country validation.
236
     * If using a package based on umpirsky/country-list, invalidate the option 'ZZ => Unknown or invalid region'.
237
     *
238
     * @param  string $country
239
     * @return bool
240
     */
241
    public function isPhoneCountry($country)
242
    {
243
        return (strlen($country) === 2 && ctype_alpha($country) && ctype_upper($country) && $country != 'ZZ');
244
    }
245
246
    /**
247
     * Checks if the supplied string is a valid phone number type.
248
     *
249
     * @param  string $type
250
     * @return bool
251
     */
252
    public function isPhoneType($type)
253
    {
254
        // Legacy support.
255
        $type = ($type == 'LANDLINE' ? 'FIXED_LINE' : $type);
256
257
        return defined('\libphonenumber\PhoneNumberType::' . $type);
258
    }
259
260
}
261