Passed
Push — master ( b45034...83eef8 )
by
unknown
03:50 queued 01:25
created

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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