Passed
Push — master ( e9d49a...8cc129 )
by Alexander
03:00
created

Locale::getBaseUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Middleware;
6
7
use DateInterval;
8
use Psr\Http\Message\ResponseFactoryInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use Psr\Log\LoggerInterface;
14
use Yiisoft\Cookies\Cookie;
15
use Yiisoft\Http\Header;
16
use Yiisoft\Http\Method;
17
use Yiisoft\Http\Status;
18
use Yiisoft\Router\UrlGeneratorInterface;
19
use Yiisoft\Session\SessionInterface;
20
use Yiisoft\Strings\WildcardPattern;
21
use Yiisoft\Translator\TranslatorInterface;
22
use Yiisoft\Yii\Middleware\Exception\InvalidLocalesFormatException;
23
24
final class Locale implements MiddlewareInterface
25
{
26
    private const DEFAULT_LOCALE = 'en';
27
    private const DEFAULT_LOCALE_NAME = '_language';
28
29
    private bool $enableSaveLocale = true;
30
    private bool $enableDetectLocale = false;
31
    private string $defaultLocale = self::DEFAULT_LOCALE;
32
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
33
    private string $sessionName = self::DEFAULT_LOCALE_NAME;
34
    private ?DateInterval $cookieDuration;
35
36
    /**
37
     * @param array $locales List of supported locales in key-value format e.g. ['ru' => 'ru_RU', 'uz' => 'uz_UZ']
38
     * @param string[] $ignoredRequests
39
     */
40 19
    public function __construct(
41
        private TranslatorInterface $translator,
42
        private UrlGeneratorInterface $urlGenerator,
43
        private SessionInterface $session,
44
        private LoggerInterface $logger,
45
        private ResponseFactoryInterface $responseFactory,
46
        private array $locales = [],
47
        private array $ignoredRequests = [],
48
        private bool $cookieSecure = false,
49
    ) {
50 19
        $this->cookieDuration = new DateInterval('P30D');
51
    }
52
53 18
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
54
    {
55 18
        if ($this->locales === []) {
56 1
            return $handler->handle($request);
57
        }
58
59 17
        $this->assertLocalesFormat();
60
61 16
        $uri = $request->getUri();
62 16
        $path = $uri->getPath();
63 16
        $query = $uri->getQuery();
64
65 16
        $locale = $this->getLocaleFromPath($path);
66
67 16
        if ($locale !== null) {
68 9
            $this->translator->setLocale($this->locales[$locale]);
69 9
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
70
71 9
            $response = $handler->handle($request);
72 9
            $newPath = null;
73 9
            if ($this->isDefaultLocale($locale) && $request->getMethod() === Method::GET) {
74 3
                $length = strlen($locale);
75 3
                $newPath = substr($path, $length + 1);
76
            }
77 9
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
78
        }
79 7
        if ($this->enableSaveLocale) {
80 7
            $locale = $this->getLocaleFromRequest($request);
81
        }
82 7
        if ($locale === null && $this->enableDetectLocale) {
83 2
            $locale = $this->detectLocale($request);
84
        }
85 7
        if ($locale === null || $this->isDefaultLocale($locale) || $this->isRequestIgnored($request)) {
86 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
87 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
88 2
            return $handler->handle($request);
89
        }
90
91 5
        $this->translator->setLocale($this->locales[$locale]);
92 5
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
93
94 5
        if ($request->getMethod() === Method::GET) {
95 4
            $location = $this->getBaseUrl() . '/' . $locale . $path . ($query !== '' ? '?' . $query : '');
96 4
            return $this->responseFactory
97 4
                ->createResponse(Status::FOUND)
98 4
                ->withHeader(Header::LOCATION, $location);
99
        }
100
101
102 1
        return $handler->handle($request);
103
    }
104
105 9
    private function applyLocaleFromPath(
106
        string $locale,
107
        ResponseInterface $response,
108
        string $query,
109
        ?string $newPath = null,
110
    ): ResponseInterface {
111 9
        if ($newPath === '') {
112 1
            $newPath = '/';
113
        }
114
115 9
        if ($newPath !== null) {
116 3
            $location = $this->getBaseUrl() . $newPath . ($query !== '' ? '?' . $query : '');
117 3
            $response = $this->responseFactory
118 3
                ->createResponse(Status::FOUND)
119 3
                ->withHeader(Header::LOCATION, $location);
120
        }
121 9
        if ($this->enableSaveLocale) {
122 9
            $response = $this->saveLocale($locale, $response);
123
        }
124 9
        return $response;
125
    }
126
127 16
    private function getLocaleFromPath(string $path): ?string
128
    {
129 16
        $parts = [];
130
        /**
131
         * @var string $code
132
         * @var string $locale
133
         */
134 16
        foreach ($this->locales as $code => $locale) {
135 16
            $parts[] = $code;
136 16
            $parts[] = $locale;
137
        }
138
139 16
        $pattern = implode('|', $parts);
140 16
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
141 9
            $matchedLocale = $matches[1];
142 9
            if (!isset($this->locales[$matchedLocale])) {
143 1
                $matchedLocale = $this->parseLocale($matchedLocale);
144
            }
145 9
            if (isset($this->locales[$matchedLocale])) {
146 9
                $this->logger->debug(sprintf("Locale '%s' found in URL", $matchedLocale));
147 9
                return $matchedLocale;
148
            }
149
        }
150 7
        return null;
151
    }
152
153 7
    private function getLocaleFromRequest(ServerRequestInterface $request): ?string
154
    {
155
        /** @var array<string, string> $cookies */
156 7
        $cookies = $request->getCookieParams();
157 7
        if (isset($cookies[$this->sessionName])) {
158 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
159 1
            return $this->parseLocale($cookies[$this->sessionName]);
160
        }
161
        /** @var array<string, string> $queryParameters */
162 6
        $queryParameters = $request->getQueryParams();
163 6
        if (isset($queryParameters[$this->queryParameterName])) {
164 4
            $this->logger->debug(
165 4
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
166 4
            );
167 4
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
168
        }
169 2
        return null;
170
    }
171
172 15
    private function isDefaultLocale(string $locale): bool
173
    {
174 15
        return $locale === $this->defaultLocale || $this->locales[$locale] === $this->defaultLocale;
175
    }
176
177 2
    private function detectLocale(ServerRequestInterface $request): ?string
178
    {
179 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
180 1
            if (!isset($this->locales[$language])) {
181 1
                $language = $this->parseLocale($language);
182
            }
183 1
            if (isset($this->locales[$language])) {
184 1
                return $language;
185
            }
186
        }
187 1
        return null;
188
    }
189
190 9
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
191
    {
192 9
        $this->logger->debug('Saving found locale to cookies');
193 9
        $this->session->set($this->sessionName, $locale);
194 9
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
195 9
        if ($this->cookieDuration !== null) {
196 9
            $cookie = $cookie->withMaxAge($this->cookieDuration);
197
        }
198 9
        return $cookie->addToResponse($response);
199
    }
200
201 7
    private function parseLocale(string $locale): string
202
    {
203 7
        if (str_contains($locale, '-')) {
204 2
            [$locale] = explode('-', $locale, 2);
205 5
        } elseif (str_contains($locale, '_')) {
206 1
            [$locale] = explode('_', $locale, 2);
207
        }
208
209 7
        return $locale;
210
    }
211
212 6
    private function isRequestIgnored(ServerRequestInterface $request): bool
213
    {
214 6
        foreach ($this->ignoredRequests as $ignoredRequest) {
215 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
216 1
                return true;
217
            }
218
        }
219 5
        return false;
220
    }
221
222
    /**
223
     * @psalm-assert array<string, string> $this->locales
224
     *
225
     * @throws InvalidLocalesFormatException
226
     */
227 17
    private function assertLocalesFormat(): void
228
    {
229 17
        foreach ($this->locales as $code => $locale) {
230 17
            if (!is_string($code) || !is_string($locale)) {
231 1
                throw new InvalidLocalesFormatException();
232
            }
233
        }
234
    }
235
236 7
    private function getBaseUrl(): string
237
    {
238 7
        return rtrim($this->urlGenerator->getUriPrefix(), '/');
239
    }
240
241
    /**
242
     * @param array $locales List of supported locales in key-value format e.g. ['ru' => 'ru_RU', 'uz' => 'uz_UZ']
243
     *
244
     * @return $this
245
     */
246 1
    public function withLocales(array $locales): self
247
    {
248 1
        $new = clone $this;
249 1
        $new->locales = $locales;
250 1
        return $new;
251
    }
252
253 3
    public function withDefaultLocale(string $defaultLocale): self
254
    {
255 3
        $new = clone $this;
256 3
        $new->defaultLocale = $defaultLocale;
257 3
        return $new;
258
    }
259
260 1
    public function withQueryParameterName(string $queryParameterName): self
261
    {
262 1
        $new = clone $this;
263 1
        $new->queryParameterName = $queryParameterName;
264 1
        return $new;
265
    }
266
267 1
    public function withSessionName(string $sessionName): self
268
    {
269 1
        $new = clone $this;
270 1
        $new->sessionName = $sessionName;
271 1
        return $new;
272
    }
273
274 1
    public function withEnableSaveLocale(bool $enableSaveLocale): self
275
    {
276 1
        $new = clone $this;
277 1
        $new->enableSaveLocale = $enableSaveLocale;
278 1
        return $new;
279
    }
280
281 3
    public function withEnableDetectLocale(bool $enableDetectLocale): self
282
    {
283 3
        $new = clone $this;
284 3
        $new->enableDetectLocale = $enableDetectLocale;
285 3
        return $new;
286
    }
287
288
    /**
289
     * @param string[] $ignoredRequests
290
     *
291
     * @return $this
292
     */
293 2
    public function withIgnoredRequests(array $ignoredRequests): self
294
    {
295 2
        $new = clone $this;
296 2
        $new->ignoredRequests = $ignoredRequests;
297 2
        return $new;
298
    }
299
300 1
    public function withCookieSecure(bool $secure): self
301
    {
302 1
        $new = clone $this;
303 1
        $new->cookieSecure = $secure;
304 1
        return $new;
305
    }
306
}
307