Passed
Pull Request — master (#63)
by
unknown
02:46
created

Locale::process()   C

Complexity

Conditions 13
Paths 19

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 13

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 31
c 1
b 0
f 0
nc 19
nop 2
dl 0
loc 48
ccs 32
cts 32
cp 1
crap 13
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
25
 * Locale middleware supports locale-based routing and configures translator and URL generator.
26
 * You should place it before `Route` middleware in the middleware list.
27
 */
28
final class Locale implements MiddlewareInterface
29
{
30
    private const DEFAULT_LOCALE = 'en';
31
    private const DEFAULT_LOCALE_NAME = '_language';
32
    private const LOCALE_SEPARATORS = ['-', '_'];
33
34
    private bool $saveLocale = true;
35
    private bool $detectLocale = false;
36
    private string $defaultLocale = self::DEFAULT_LOCALE;
37
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
38
    private string $sessionName = self::DEFAULT_LOCALE_NAME;
39
    private DateInterval $cookieDuration;
40
    /**
41
     * @psalm-var array<string, string>
42
     */
43
    private array $supportedLocales;
44
45
    /**
46
     * @param TranslatorInterface $translator Translator instance to set locale for.
47
     * @param UrlGeneratorInterface $urlGenerator URL generator instance to set locale for.
48
     * @param SessionInterface $session Session instance to save locale to.
49
     * @param LoggerInterface $logger Logger instance to write debug logs to.
50
     * @param ResponseFactoryInterface $responseFactory Response factory used to create redirect responses.
51
     * @param array $supportedLocales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
52
     * @param string[] $ignoredRequestUrlPatterns {@see WildcardPattern Patterns} for ignoring requests with URLs matching.
53
     * @param bool $secureCookie Whether middleware should flag locale cookie as "secure". Effective only when
54
     * {@see $saveLocale} is set to `true`.
55
     * @param ?DateInterval $cookieDuration Locale cookie lifetime. Effective only when {@see $saveLocale} is set to
56
     * `true`. Defaults to 30 days.
57
     */
58 19
    public function __construct(
59
        private TranslatorInterface $translator,
60
        private UrlGeneratorInterface $urlGenerator,
61
        private SessionInterface $session,
62
        private LoggerInterface $logger,
63
        private ResponseFactoryInterface $responseFactory,
64
        array $supportedLocales = [],
65
        private array $ignoredRequestUrlPatterns = [],
66
        private bool $secureCookie = false,
67
        ?DateInterval $cookieDuration = null,
68
    ) {
69 19
        $this->cookieDuration = $cookieDuration ?? new DateInterval('P30D');
70
71 19
        $this->assertSupportedLocalesFormat($supportedLocales);
72 18
        $this->supportedLocales = $supportedLocales;
73
    }
74
75 17
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
76
    {
77 17
        if (empty($this->supportedLocales)) {
78 1
            return $handler->handle($request);
79
        }
80
81 16
        $uri = $request->getUri();
82 16
        $path = $uri->getPath();
83 16
        $query = $uri->getQuery();
84
85 16
        $locale = $this->getLocaleFromPath($path);
86
87 16
        if ($locale !== null) {
88 9
            $this->translator->setLocale($this->supportedLocales[$locale]);
89 9
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
90
91 9
            $response = $handler->handle($request);
92 9
            $newPath = null;
93 9
            if ($this->isDefaultLocale($locale) && $request->getMethod() === Method::GET) {
94 3
                $length = strlen($locale);
95 3
                $newPath = substr($path, $length + 1);
96
            }
97 9
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
98
        }
99 7
        if ($this->saveLocale) {
100 7
            $locale = $this->getLocaleFromRequest($request);
101
        }
102 7
        if ($locale === null && $this->detectLocale) {
103 2
            $locale = $this->detectLocale($request);
104
        }
105 7
        if ($locale === null || $this->isDefaultLocale($locale) || $this->isRequestIgnored($request)) {
106 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
107 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
108 2
            return $handler->handle($request);
109
        }
110
111 5
        $this->translator->setLocale($this->supportedLocales[$locale]);
112 5
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
113
114 5
        if ($request->getMethod() === Method::GET) {
115 4
            $location = $this->getBaseUrl() . '/' . $locale . $path . ($query !== '' ? '?' . $query : '');
116 4
            return $this->responseFactory
117 4
                ->createResponse(Status::FOUND)
118 4
                ->withHeader(Header::LOCATION, $location);
119
        }
120
121
122 1
        return $handler->handle($request);
123
    }
124
125 9
    private function applyLocaleFromPath(
126
        string $locale,
127
        ResponseInterface $response,
128
        string $query,
129
        ?string $newPath = null,
130
    ): ResponseInterface {
131 9
        if ($newPath === '') {
132 1
            $newPath = '/';
133
        }
134
135 9
        if ($newPath !== null) {
136 3
            $location = $this->getBaseUrl() . $newPath . ($query !== '' ? '?' . $query : '');
137 3
            $response = $this->responseFactory
138 3
                ->createResponse(Status::FOUND)
139 3
                ->withHeader(Header::LOCATION, $location);
140
        }
141 9
        if ($this->saveLocale) {
142 9
            $response = $this->saveLocale($locale, $response);
143
        }
144 9
        return $response;
145
    }
146
147 16
    private function getLocaleFromPath(string $path): ?string
148
    {
149 16
        $parts = [];
150 16
        foreach ($this->supportedLocales as $code => $locale) {
151 16
            $parts[] = $code;
152 16
            $parts[] = $locale;
153
        }
154
155 16
        $pattern = implode('|', $parts);
156 16
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
157 9
            $matchedLocale = $matches[1];
158 9
            if (!isset($this->supportedLocales[$matchedLocale])) {
159 1
                $matchedLocale = $this->parseLocale($matchedLocale);
160
            }
161 9
            if (isset($this->supportedLocales[$matchedLocale])) {
162 9
                $this->logger->debug(sprintf("Locale '%s' found in URL", $matchedLocale));
163 9
                return $matchedLocale;
164
            }
165
        }
166 7
        return null;
167
    }
168
169 7
    private function getLocaleFromRequest(ServerRequestInterface $request): ?string
170
    {
171
        /** @var array<string, string> $cookies */
172 7
        $cookies = $request->getCookieParams();
173 7
        if (isset($cookies[$this->sessionName])) {
174 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
175 1
            return $this->parseLocale($cookies[$this->sessionName]);
176
        }
177
        /** @var array<string, string> $queryParameters */
178 6
        $queryParameters = $request->getQueryParams();
179 6
        if (isset($queryParameters[$this->queryParameterName])) {
180 4
            $this->logger->debug(
181 4
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
182 4
            );
183 4
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
184
        }
185 2
        return null;
186
    }
187
188 15
    private function isDefaultLocale(string $locale): bool
189
    {
190 15
        return $locale === $this->defaultLocale || $this->supportedLocales[$locale] === $this->defaultLocale;
191
    }
192
193 2
    private function detectLocale(ServerRequestInterface $request): ?string
194
    {
195 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
196 1
            if (!isset($this->supportedLocales[$language])) {
197 1
                $language = $this->parseLocale($language);
198
            }
199 1
            if (isset($this->supportedLocales[$language])) {
200 1
                return $language;
201
            }
202
        }
203 1
        return null;
204
    }
205
206 9
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
207
    {
208 9
        $this->logger->debug('Saving found locale to session and cookies.');
209 9
        $this->session->set($this->sessionName, $locale);
210 9
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->secureCookie);
211 9
        $cookie = $cookie->withMaxAge($this->cookieDuration);
212 9
        return $cookie->addToResponse($response);
213
    }
214
215 7
    private function parseLocale(string $locale): string
216
    {
217 7
        foreach (self::LOCALE_SEPARATORS as $separator) {
218 7
            if (str_contains($locale, $separator)) {
219 3
                return explode($separator, $locale, 2)[0];
220
            }
221
        }
222
223 4
        return $locale;
224
    }
225
226 6
    private function isRequestIgnored(ServerRequestInterface $request): bool
227
    {
228 6
        foreach ($this->ignoredRequestUrlPatterns as $ignoredRequest) {
229 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
230 1
                return true;
231
            }
232
        }
233 5
        return false;
234
    }
235
236
    /**
237
     * @psalm-assert array<string, string> $supportedLocales
238
     *
239
     * @throws InvalidLocalesFormatException
240
     */
241 19
    private function assertSupportedLocalesFormat(array $supportedLocales): void
242
    {
243 19
        foreach ($supportedLocales as $code => $locale) {
244 18
            if (!is_string($code) || !is_string($locale)) {
245 1
                throw new InvalidLocalesFormatException();
246
            }
247
        }
248
    }
249
250 7
    private function getBaseUrl(): string
251
    {
252 7
        return rtrim($this->urlGenerator->getUriPrefix(), '/');
253
    }
254
255
    /**
256
     * Return new instance with supported locales specified.
257
     *
258
     * @param array $locales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
259
     *
260
     * @throws InvalidLocalesFormatException
261
     */
262 1
    public function withSupportedLocales(array $locales): self
263
    {
264 1
        $this->assertSupportedLocalesFormat($locales);
265 1
        $new = clone $this;
266 1
        $new->supportedLocales = $locales;
267 1
        return $new;
268
    }
269
270
    /**
271
     * Return new instance with default locale specified.
272
     *
273
     * @param string $defaultLocale Default locale.
274
     */
275 3
    public function withDefaultLocale(string $defaultLocale): self
276
    {
277 3
        $new = clone $this;
278 3
        $new->defaultLocale = $defaultLocale;
279 3
        return $new;
280
    }
281
282
    /**
283
     * Return new instance with the name of the query string parameter to look for locale.
284
     *
285
     * @param string $queryParameterName Name of the query string parameter.
286
     */
287 1
    public function withQueryParameterName(string $queryParameterName): self
288
    {
289 1
        $new = clone $this;
290 1
        $new->queryParameterName = $queryParameterName;
291 1
        return $new;
292
    }
293
294
    /**
295
     * Return new instance with the name of session parameter to store found locale.
296
     *
297
     * @param string $sessionName Name of session parameter.
298
     */
299 1
    public function withSessionName(string $sessionName): self
300
    {
301 1
        $new = clone $this;
302 1
        $new->sessionName = $sessionName;
303 1
        return $new;
304
    }
305
306
    /**
307
     * Return new instance with enabled or disabled saving of locale.
308
     *
309
     * @param bool $enabled Whether middleware should save locale into session and cookies.
310
     */
311 1
    public function withSaveLocale(bool $enabled): self
312
    {
313 1
        $new = clone $this;
314 1
        $new->saveLocale = $enabled;
315 1
        return $new;
316
    }
317
318
    /**
319
     * Return new instance with enabled or disabled detection of locale based on `Accept-Language` header.
320
     *
321
     * @param bool $enabled Whether middleware should detect locale.
322
     */
323 3
    public function withDetectLocale(bool $enabled): self
324
    {
325 3
        $new = clone $this;
326 3
        $new->detectLocale = $enabled;
327 3
        return $new;
328
    }
329
330
    /**
331
     * Return new instance with {@see WildcardPattern patterns} for ignoring requests with URLs matching.
332
     *
333
     * @param string[] $patterns Patterns.
334
     */
335 2
    public function withIgnoredRequestUrlPatterns(array $patterns): self
336
    {
337 2
        $new = clone $this;
338 2
        $new->ignoredRequestUrlPatterns = $patterns;
339 2
        return $new;
340
    }
341
342
    /**
343
     * Return new instance with enabled or disabled secure cookies.
344
     *
345
     * @param bool $secure Whether middleware should flag locale cookie as "secure."
346
     */
347 1
    public function withSecureCookie(bool $secure): self
348
    {
349 1
        $new = clone $this;
350 1
        $new->secureCookie = $secure;
351 1
        return $new;
352
    }
353
}
354