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

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 3
b 0
f 0
nc 1
nop 9
dl 0
loc 15
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". 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