Completed
Push — master ( 11d1cc...790a65 )
by ARCANEDEV
7s
created

Negotiator::inSupportedLocales()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 5
nop 1
dl 0
loc 16
ccs 10
cts 10
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
1
<?php namespace Arcanedev\Localization\Utilities;
2
3
use Arcanedev\Localization\Contracts\Negotiator as NegotiatorContract;
4
use Arcanedev\Localization\Entities\LocaleCollection;
5
use Illuminate\Http\Request;
6
use Locale;
7
8
/**
9
 * Class     Negotiator
10
 *
11
 * @package  Arcanedev\Localization\Utilities
12
 * @author   ARCANEDEV <[email protected]>
13
 *
14
 * Negotiates language with the user's browser through the Accept-Language HTTP header or the user's host address.
15
 * Language codes are generally in the form "ll" for a language spoken in only one country, or "ll-CC" for a
16
 * language spoken in a particular country.  For example, U.S. English is "en-US", while British English is "en-UK".
17
 * Portuguese as spoken in Portugal is "pt-PT", while Brazilian Portuguese is "pt-BR".
18
 *
19
 * This function is based on negotiateLanguage from Pear HTTP2
20
 * http://pear.php.net/package/HTTP2/
21
 *
22
 * Quality factors in the Accept-Language: header are supported, e.g.:
23
 * Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8
24
 */
25
class Negotiator implements NegotiatorContract
26
{
27
    /* ------------------------------------------------------------------------------------------------
28
     |  Properties
29
     | ------------------------------------------------------------------------------------------------
30
     */
31
    /**
32
     * Default Locale.
33
     *
34
     * @var string
35
     */
36
    private $defaultLocale;
37
38
    /**
39
     * The supported locales collection.
40
     *
41
     * @var \Arcanedev\Localization\Entities\LocaleCollection
42
     */
43
    private $supportedLocales;
44
45
    /**
46
     * The HTTP request instance.
47
     *
48
     * @var \Illuminate\Http\Request
49
     */
50
    private $request;
51
52
    /* ------------------------------------------------------------------------------------------------
53
     |  Main Functions
54
     | ------------------------------------------------------------------------------------------------
55
     */
56
    /**
57
     * Make Negotiator instance.
58
     *
59
     * @param  string                                             $defaultLocale
60
     * @param  \Arcanedev\Localization\Entities\LocaleCollection  $supportedLanguages
61
     */
62 189
    public function __construct($defaultLocale, LocaleCollection $supportedLanguages)
63
    {
64 189
        $this->defaultLocale    = $defaultLocale;
65 189
        $this->supportedLocales = $supportedLanguages;
66 189
    }
67
68
    /* ------------------------------------------------------------------------------------------------
69
     |  Main Functions
70
     | ------------------------------------------------------------------------------------------------
71
     */
72
    /**
73
     * Negotiate the request.
74
     *
75
     * @param  \Illuminate\Http\Request  $request
76
     *
77
     * @return string
78
     */
79 189
    public function negotiate(Request $request)
80
    {
81 189
        $this->request = $request;
82
83 189
        $locale = $this->getFromAcceptedLanguagesHeader();
84
85 189
        if ( ! is_null($locale)) return $locale;
86
87 12
        $locale = $this->getFromHttpAcceptedLanguagesServer();
88
89 12
        if ( ! is_null($locale)) return $locale;
90
91 9
        $locale = $this->getFromRemoteHostServer();
92
93 9
        if ( ! is_null($locale)) return $locale;
94
95
        // TODO: Adding negotiate form IP Address ??
96
97 6
        return $this->defaultLocale;
98
    }
99
100
    /**
101
     * Get locale from accepted languages header.
102
     *
103
     * @return null|string
104
     */
105 189
    private function getFromAcceptedLanguagesHeader()
106
    {
107 189
        $matches = $this->getMatchesFromAcceptedLanguages();
108
109 189
        if ($locale = $this->inSupportedLocales($matches)) {
110 189
            return $locale;
111
        }
112
113
        // If any (i.e. "*") is acceptable, return the first supported locale
114 15
        if (isset($matches['*'])) {
115 3
            return $this->supportedLocales->first()->key();
116
        }
117
118 12
        return null;
119
    }
120
121
    /**
122
     * Get locale from http accepted languages server.
123
     *
124
     * @return null|string
125
     */
126 12
    private function getFromHttpAcceptedLanguagesServer()
127
    {
128 12
        $httpAcceptLanguage = $this->request->server('HTTP_ACCEPT_LANGUAGE');
129
130
        // @codeCoverageIgnoreStart
131
        if ( ! class_exists('Locale') || empty($httpAcceptLanguage)) {
132
            return null;
133
        }
134
        // @codeCoverageIgnoreEnd
135
136 12
        $locale = Locale::acceptFromHttp($httpAcceptLanguage);
137
138 12
        if ($this->isSupported($locale)) {
139 3
            return $locale;
140
        }
141
142 9
        return null;
143
    }
144
145
    /**
146
     * Get locale from remote host server.
147
     *
148
     * @return null|string
149
     */
150 9
    private function getFromRemoteHostServer()
151
    {
152 9
        if (empty($remoteHost = $this->request->server('REMOTE_HOST')))
153 5
            return null;
154
155 6
        $remoteHost = explode('.', $remoteHost);
156 6
        $locale     = strtolower(end($remoteHost));
157
158 6
        return $this->isSupported($locale) ? $locale : null;
159
    }
160
161
    /* ------------------------------------------------------------------------------------------------
162
     |  Check Functions
163
     | ------------------------------------------------------------------------------------------------
164
     */
165
    /**
166
     * Check if matches a supported locale.
167
     *
168
     * @param  array  $matches
169
     *
170
     * @return null|string
171
     */
172 189
    private function inSupportedLocales(array $matches)
173
    {
174 189
        foreach (array_keys($matches) as $locale) {
175 189
            if ($this->isSupported($locale))
176 189
                return $locale;
177
178
            // Search for acceptable locale by 'regional' => 'fr_FR' match.
179 189
            foreach ($this->supportedLocales as $key => $entity) {
180
                /** @var \Arcanedev\Localization\Entities\Locale $entity */
181 189
                if ($entity->regional() == $locale)
182 189
                    return $key;
183 63
            }
184 63
        }
185
186 15
        return null;
187
    }
188
189
    /**
190
     * Check if the locale is supported.
191
     *
192
     * @param  string  $locale
193
     *
194
     * @return bool
195
     */
196 189
    private function isSupported($locale)
197
    {
198 189
        return $this->supportedLocales->has($locale);
199
    }
200
201
    /* ------------------------------------------------------------------------------------------------
202
     |  Other Functions
203
     | ------------------------------------------------------------------------------------------------
204
     */
205
    /**
206
     * Return all the accepted languages from the browser
207
     *
208
     * @return array  -  Matches from the header field Accept-Languages
209
     */
210 189
    private function getMatchesFromAcceptedLanguages()
211
    {
212 189
        $matches = [];
213
214 189
        $acceptLanguages = $this->request->header('Accept-Language');
215
216 189
        if ( ! empty($acceptLanguages)) {
217 189
            $acceptLanguages = explode(',', $acceptLanguages);
218
219 189
            $genericMatches = $this->retrieveGenericMatches($acceptLanguages, $matches);
220
221 189
            $matches = array_merge($genericMatches, $matches);
222 189
            arsort($matches, SORT_NUMERIC);
223 63
        }
224
225 189
        return $matches;
226
    }
227
228
    /**
229
     * Get the generic matches.
230
     *
231
     * @param  array  $acceptLanguages
232
     * @param  array  $matches
233
     *
234
     * @return array
235
     */
236 189
    private function retrieveGenericMatches($acceptLanguages, &$matches)
237
    {
238 189
        $genericMatches = [];
239
240 189
        foreach ($acceptLanguages as $option) {
241 189
            $option  = array_map('trim', explode(';', $option));
242 189
            $locale  = $option[0];
243 189
            $quality = $this->getQualityFactor($locale, $option);
244
245
            // Unweighted values, get high weight by their position in the list
246 189
            $quality          = isset($quality) ? $quality : 1000 - count($matches);
247 189
            $matches[$locale] = $quality;
248
249
            // If for some reason the Accept-Language header only sends language with country we should make
250
            // the language without country an accepted option, with a value less than it's parent.
251 189
            $localeOptions = explode('-', $locale);
252 189
            array_pop($localeOptions);
253
254 189
            while ( ! empty($localeOptions)) {
255
                //The new generic option needs to be slightly less important than it's base
256 189
                $quality -= 0.001;
257 189
                $opt      = implode('-', $localeOptions);
258
259 189
                if (empty($genericMatches[$opt]) || $genericMatches[$opt] > $quality) {
260 189
                    $genericMatches[$opt] = $quality;
261 63
                }
262
263 189
                array_pop($localeOptions);
264 63
            }
265 63
        }
266
267 189
        return $genericMatches;
268
    }
269
270
    /**
271
     * Get the quality factor.
272
     *
273
     * @param  string  $locale
274
     * @param  array   $option
275
     *
276
     * @return float|null
277
     */
278 189
    private function getQualityFactor($locale, $option)
279
    {
280 189
        if (isset($option[1])) {
281 189
            return (float) str_replace('q=', '', $option[1]);
282
        }
283
284
        // Assign default low weight for generic values
285 189
        if ($locale === '*/*') {
286 3
            return 0.01;
287
        }
288
289 189
        if (substr($locale, -1) === '*') {
290 3
            return 0.02;
291
        }
292
293 189
        return null;
294
    }
295
}
296