Passed
Pull Request — master (#81)
by
unknown
09:33 queued 06:31
created

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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