Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.54% covered (danger)
47.54%
29 / 61
64.29% covered (warning)
64.29%
9 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Locales
47.54% covered (danger)
47.54%
29 / 61
64.29% covered (warning)
64.29%
9 / 14
211.85
0.00% covered (danger)
0.00%
0 / 1
 load
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 current
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 next
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefault
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 negotiate
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 setNegotiator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fromRequest
68.42% covered (warning)
68.42%
13 / 19
0.00% covered (danger)
0.00%
0 / 1
14.81
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromBrowser
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray;
6
7use Celemas\Core\App;
8use Celemas\Core\Plugin as CorePlugin;
9use Closure;
10use Cosray\Exception\RuntimeException;
11use Cosray\Middleware\AddLocale;
12use Iterator;
13use Psr\Http\Message\ServerRequestInterface as Request;
14
15class Locales implements Iterator, CorePlugin
16{
17    /** @var array<string, Locale> */
18    protected array $locales = [];
19
20    protected ?string $default = null;
21    protected ?Closure $negotiator = null;
22
23    public function load(App $app): void
24    {
25        $app->middleware(new AddLocale($this));
26        $app->register(self::class, $this);
27    }
28
29    public function add(
30        string $id,
31        string $title,
32        ?string $fallback = null,
33        ?string $pgDict = null,
34        ?array $domains = null,
35        ?string $urlPrefix = null,
36    ) {
37        // The first locale is always the default one
38        if ($this->default === null) {
39            $this->default = $id;
40        }
41
42        $this->locales[$id] = new Locale($this, $id, $title, $fallback, $pgDict, $domains, $urlPrefix);
43    }
44
45    public function get(string $id): Locale
46    {
47        return $this->locales[$id];
48    }
49
50    public function rewind(): void
51    {
52        reset($this->locales);
53    }
54
55    public function current(): Locale
56    {
57        return current($this->locales);
58    }
59
60    public function key(): string
61    {
62        return key($this->locales);
63    }
64
65    public function next(): void
66    {
67        next($this->locales);
68    }
69
70    public function valid(): bool
71    {
72        return key($this->locales) !== null;
73    }
74
75    public function getDefault(): Locale
76    {
77        // default locale from config file
78        if ($this->default === null) {
79            throw new RuntimeException('Default locale not available');
80        }
81
82        return $this->locales[$this->default];
83    }
84
85    public function negotiate(Request $request): Locale
86    {
87        if ($this->negotiator) {
88            return ($this->negotiator)($request, $this->locales, $this->getDefault());
89        }
90
91        return $this->fromRequest($request, $this->locales, $this->getDefault());
92    }
93
94    public function setNegotiator(Closure $func): void
95    {
96        $this->negotiator = $func;
97    }
98
99    protected function fromRequest(Request $request, array $locales, Locale $default): Locale
100    {
101        $uri = $request->getUri();
102
103        // From query paramter
104        $locale = $request->getQueryParams()['locale'] ?? null;
105
106        if ($locale && $this->exists($locale)) {
107            return $locales[$locale];
108        }
109
110        // By domain
111        $host = strtolower(explode(':', $uri->getHost())[0]);
112
113        foreach ($locales as $locale) {
114            foreach ($locale->domains as $domain) {
115                if ($host === $domain) {
116                    return $locale;
117                }
118            }
119        }
120
121        // From URL path prefix. e. g. http://example.com/en_EN/path/to/page
122        $prefix = explode('/', trim($uri->getPath(), '/'))[0];
123
124        foreach ($locales as $locale) {
125            if ($prefix === $locale->urlPrefix) {
126                return $locale;
127            }
128        }
129
130        // From session
131        $session = $request->getAttribute('session', null);
132
133        if ($session) {
134            $locale = $session->get('locale', false);
135
136            if ($locale && $this->exists($locale)) {
137                return $locales[$locale];
138            }
139        }
140
141        // From the locales the browser says the user accepts
142        // $locale = $this->fromBrowser();
143        // if ($locale && $this->exists($locale)) {
144        //    return $locales[$locale];
145        // }
146
147        return $default;
148    }
149
150    protected function exists(string $id): bool
151    {
152        return array_key_exists($id, $this->locales);
153    }
154
155    protected function fromBrowser(): string|false
156    {
157        if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
158            preg_match_all(
159                '/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
160                $_SERVER['HTTP_ACCEPT_LANGUAGE'],
161                $matches,
162            );
163
164            if (count($matches[1])) {
165                $langs = array_combine($matches[1], $matches[4]);
166
167                foreach ($langs as $lang => $val) {
168                    if ($val !== '') {
169                        continue;
170                    }
171
172                    $langs[$lang] = 1;
173                }
174
175                arsort($langs, SORT_NUMERIC);
176
177                foreach ($langs as $lang => $val) {
178                    if ($this->exists($lang)) {
179                        return $lang;
180                    }
181
182                    $lang = str_replace('-', '_', $lang);
183
184                    if ($this->exists($lang)) {
185                        return $lang;
186                    }
187
188                    $lang = strtok($lang, '_');
189
190                    if ($this->exists($lang)) {
191                        return $lang;
192                    }
193                }
194            }
195        }
196
197        return false;
198    }
199}