TongueDetector::canonicalize()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 5
eloc 12
c 2
b 1
f 0
nc 6
nop 1
dl 0
loc 20
rs 9.5555
1
<?php
2
3
namespace Pmochine\LaravelTongue\Localization;
4
5
use Illuminate\Http\Request;
6
use Locale;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Pmochine\LaravelTongue\Localization\Locale. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
7
8
/**
9
 * Class was originally written by Mcamara.
10
 * Visit the package!
11
 * https://github.com/mcamara/laravel-localization.
12
 */
13
class TongueDetector
14
{
15
    /**
16
     * @var string
17
     */
18
    private $fallbackLocale;
19
    /**
20
     * @var array
21
     */
22
    private $supportedLanguages;
23
    /**
24
     * @var Request
25
     */
26
    private $request;
27
    /**
28
     * @var bool
29
     */
30
    private $use_intl = false;
31
32
    /**
33
     * @param  string  $fallbackLocale
34
     * @param  array  $supportedLanguages
35
     * @param  Request  $request
36
     */
37
    public function __construct($fallbackLocale, $supportedLanguages, Request $request)
38
    {
39
        $this->fallbackLocale = $fallbackLocale;
40
41
        $this->canonicalize($supportedLanguages);
42
43
        $this->request = $request;
44
    }
45
46
    /**
47
     * Negotiates language with the user's browser through the Accept-Language
48
     * HTTP header or the user's host address.  Language codes are generally in
49
     * the form "ll" for a language spoken in only one country, or "ll-CC" for a
50
     * language spoken in a particular country.  For example, U.S. English is
51
     * "en-US", while British English is "en-UK".  Portuguese as spoken in
52
     * Portugal is "pt-PT", while Brazilian Portuguese is "pt-BR".
53
     *
54
     * This function is based on negotiateLanguage from Pear HTTP2
55
     * http://pear.php.net/package/HTTP2/
56
     *
57
     * Quality factors in the Accept-Language: header are supported, e.g.:
58
     *      Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8
59
     *
60
     * @return string The negotiated language result or app.locale.
61
     */
62
    public function negotiateLanguage()
63
    {
64
        $matches = $this->getMatchesFromAcceptedLanguages();
65
66
        foreach ($matches as $key => $q) {
67
            if (! empty($this->supportedLanguages[$key])) {
68
                return $key;
69
            }
70
71
            if ($this->use_intl) {
72
                $key = Locale::canonicalize($key);
73
            }
74
75
            // Search for acceptable locale by 'regional' => 'af_ZA' or 'lang' => 'af-ZA' match.
76
            foreach ($this->supportedLanguages as $key_supported => $locale) {
77
                if ((isset($locale['regional']) && $locale['regional'] == $key)
78
                    || (isset($locale['lang']) && $locale['lang'] == $key)
79
                ) {
80
                    return $key_supported;
81
                }
82
            }
83
        }
84
85
        // If any (i.e. "*") is acceptable, return the first supported format
86
        if (isset($matches['*'])) {
87
            reset($this->supportedLanguages);
88
89
            return key($this->supportedLanguages);
90
        }
91
92
        if ($this->use_intl && ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
93
            $http_accept_language = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
94
95
            if (! empty($this->supportedLanguages[$http_accept_language])) {
96
                return $http_accept_language;
97
            }
98
        }
99
100
        if ($this->request->server('REMOTE_HOST')) {
101
            $remote_host = explode('.', $this->request->server('REMOTE_HOST'));
0 ignored issues
show
Bug introduced by
It seems like $this->request->server('REMOTE_HOST') can also be of type array and null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
            $remote_host = explode('.', /** @scrutinizer ignore-type */ $this->request->server('REMOTE_HOST'));
Loading history...
102
            $lang = strtolower(end($remote_host));
103
104
            if (! empty($this->supportedLanguages[$lang])) {
105
                return $lang;
106
            }
107
        }
108
109
        return $this->fallbackLocale;
110
    }
111
112
    /**
113
     * Return all the accepted languages from the browser.
114
     *
115
     * @return array Matches from the header field Accept-Languages
116
     */
117
    private function getMatchesFromAcceptedLanguages()
118
    {
119
        $matches = [];
120
121
        if ($acceptLanguages = $this->request->header('Accept-Language')) {
122
            $acceptLanguages = explode(',', $acceptLanguages);
0 ignored issues
show
Bug introduced by
It seems like $acceptLanguages can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
            $acceptLanguages = explode(',', /** @scrutinizer ignore-type */ $acceptLanguages);
Loading history...
123
            $generic_matches = [];
124
125
            foreach ($acceptLanguages as $option) {
126
                $option = array_map('trim', explode(';', $option));
127
                $l = $option[0];
128
                if (isset($option[1])) {
129
                    $q = (float) str_replace('q=', '', $option[1]);
130
                } else {
131
                    $q = null;
132
                    // Assign default low weight for generic values
133
                    if ($l == '*/*') {
134
                        $q = 0.01;
135
                    } elseif (substr($l, -1) == '*') {
136
                        $q = 0.02;
137
                    }
138
                }
139
                // Unweighted values, get high weight by their position in the
140
                // list
141
                $q = isset($q) ? $q : 1000 - count($matches);
142
                $matches[$l] = $q;
143
                //If for some reason the Accept-Language header only sends language with country
144
                //we should make the language without country an accepted option, with a value
145
                //less than it's parent.
146
                $l_ops = explode('-', $l);
147
                array_pop($l_ops);
148
                while (! empty($l_ops)) {
149
                    //The new generic option needs to be slightly less important than it's base
150
                    $q -= 0.001;
151
                    $op = implode('-', $l_ops);
152
                    if (empty($generic_matches[$op]) || $generic_matches[$op] > $q) {
153
                        $generic_matches[$op] = $q;
154
                    }
155
                    array_pop($l_ops);
156
                }
157
            }
158
            $matches = array_merge($generic_matches, $matches);
159
            arsort($matches, SORT_NUMERIC);
160
        }
161
162
        return $matches;
163
    }
164
165
    /**
166
     * Set the supportedLanguages using
167
     * http://php.net/manual/de/locale.canonicalize.php.
168
     */
169
    protected function canonicalize($supportedLanguages)
170
    {
171
        if (class_exists('Locale')) {
172
            $this->use_intl = true;
173
174
            foreach ($supportedLanguages as $key => $language) {
175
                if (isset($language['lang'])) {
176
                    $language['lang'] = Locale::canonicalize($language['lang']);
177
                } else {
178
                    $language['lang'] = Locale::canonicalize($key);
179
                }
180
181
                if (isset($language['regional'])) {
182
                    $language['regional'] = Locale::canonicalize($language['regional']);
183
                }
184
185
                $this->supportedLanguages[$key] = $language;
186
            }
187
        } else {
188
            $this->supportedLanguages = $supportedLanguages;
189
        }
190
    }
191
}
192