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

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 2
b 0
f 0
nc 1
nop 8
dl 0
loc 14
ccs 4
cts 4
cp 1
crap 1
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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