Completed
Push — master ( fe9181...bc0bfa )
by ARCANEDEV
12s
created

Negotiator::make()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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