Passed
Push — master ( e9d49a...8cc129 )
by Alexander
03:00
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
final class Locale implements MiddlewareInterface
25
{
26
    private const DEFAULT_LOCALE = 'en';
27
    private const DEFAULT_LOCALE_NAME = '_language';
28
29
    private bool $enableSaveLocale = true;
30
    private bool $enableDetectLocale = false;
31
    private string $defaultLocale = self::DEFAULT_LOCALE;
32
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
33
    private string $sessionName = self::DEFAULT_LOCALE_NAME;
34
    private ?DateInterval $cookieDuration;
35
36
    /**
37
     * @param array $locales List of supported locales in key-value format e.g. ['ru' => 'ru_RU', 'uz' => 'uz_UZ']
38
     * @param string[] $ignoredRequests
39
     */
40 19
    public function __construct(
41
        private TranslatorInterface $translator,
42
        private UrlGeneratorInterface $urlGenerator,
43
        private SessionInterface $session,
44
        private LoggerInterface $logger,
45
        private ResponseFactoryInterface $responseFactory,
46
        private array $locales = [],
47
        private array $ignoredRequests = [],
48
        private bool $cookieSecure = false,
49
    ) {
50 19
        $this->cookieDuration = new DateInterval('P30D');
51
    }
52
53 18
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
54
    {
55 18
        if ($this->locales === []) {
56 1
            return $handler->handle($request);
57
        }
58
59 17
        $this->assertLocalesFormat();
60
61 16
        $uri = $request->getUri();
62 16
        $path = $uri->getPath();
63 16
        $query = $uri->getQuery();
64
65 16
        $locale = $this->getLocaleFromPath($path);
66
67 16
        if ($locale !== null) {
68 9
            $this->translator->setLocale($this->locales[$locale]);
69 9
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
70
71 9
            $response = $handler->handle($request);
72 9
            $newPath = null;
73 9
            if ($this->isDefaultLocale($locale) && $request->getMethod() === Method::GET) {
74 3
                $length = strlen($locale);
75 3
                $newPath = substr($path, $length + 1);
76
            }
77 9
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
78
        }
79 7
        if ($this->enableSaveLocale) {
80 7
            $locale = $this->getLocaleFromRequest($request);
81
        }
82 7
        if ($locale === null && $this->enableDetectLocale) {
83 2
            $locale = $this->detectLocale($request);
84
        }
85 7
        if ($locale === null || $this->isDefaultLocale($locale) || $this->isRequestIgnored($request)) {
86 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
87 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
88 2
            return $handler->handle($request);
89
        }
90
91 5
        $this->translator->setLocale($this->locales[$locale]);
92 5
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
93
94 5
        if ($request->getMethod() === Method::GET) {
95 4
            $location = $this->getBaseUrl() . '/' . $locale . $path . ($query !== '' ? '?' . $query : '');
96 4
            return $this->responseFactory
97 4
                ->createResponse(Status::FOUND)
98 4
                ->withHeader(Header::LOCATION, $location);
99
        }
100
101
102 1
        return $handler->handle($request);
103
    }
104
105 9
    private function applyLocaleFromPath(
106
        string $locale,
107
        ResponseInterface $response,
108
        string $query,
109
        ?string $newPath = null,
110
    ): ResponseInterface {
111 9
        if ($newPath === '') {
112 1
            $newPath = '/';
113
        }
114
115 9
        if ($newPath !== null) {
116 3
            $location = $this->getBaseUrl() . $newPath . ($query !== '' ? '?' . $query : '');
117 3
            $response = $this->responseFactory
118 3
                ->createResponse(Status::FOUND)
119 3
                ->withHeader(Header::LOCATION, $location);
120
        }
121 9
        if ($this->enableSaveLocale) {
122 9
            $response = $this->saveLocale($locale, $response);
123
        }
124 9
        return $response;
125
    }
126
127 16
    private function getLocaleFromPath(string $path): ?string
128
    {
129 16
        $parts = [];
130
        /**
131
         * @var string $code
132
         * @var string $locale
133
         */
134 16
        foreach ($this->locales as $code => $locale) {
135 16
            $parts[] = $code;
136 16
            $parts[] = $locale;
137
        }
138
139 16
        $pattern = implode('|', $parts);
140 16
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
141 9
            $matchedLocale = $matches[1];
142 9
            if (!isset($this->locales[$matchedLocale])) {
143 1
                $matchedLocale = $this->parseLocale($matchedLocale);
144
            }
145 9
            if (isset($this->locales[$matchedLocale])) {
146 9
                $this->logger->debug(sprintf("Locale '%s' found in URL", $matchedLocale));
147 9
                return $matchedLocale;
148
            }
149
        }
150 7
        return null;
151
    }
152
153 7
    private function getLocaleFromRequest(ServerRequestInterface $request): ?string
154
    {
155
        /** @var array<string, string> $cookies */
156 7
        $cookies = $request->getCookieParams();
157 7
        if (isset($cookies[$this->sessionName])) {
158 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
159 1
            return $this->parseLocale($cookies[$this->sessionName]);
160
        }
161
        /** @var array<string, string> $queryParameters */
162 6
        $queryParameters = $request->getQueryParams();
163 6
        if (isset($queryParameters[$this->queryParameterName])) {
164 4
            $this->logger->debug(
165 4
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
166 4
            );
167 4
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
168
        }
169 2
        return null;
170
    }
171
172 15
    private function isDefaultLocale(string $locale): bool
173
    {
174 15
        return $locale === $this->defaultLocale || $this->locales[$locale] === $this->defaultLocale;
175
    }
176
177 2
    private function detectLocale(ServerRequestInterface $request): ?string
178
    {
179 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
180 1
            if (!isset($this->locales[$language])) {
181 1
                $language = $this->parseLocale($language);
182
            }
183 1
            if (isset($this->locales[$language])) {
184 1
                return $language;
185
            }
186
        }
187 1
        return null;
188
    }
189
190 9
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
191
    {
192 9
        $this->logger->debug('Saving found locale to cookies');
193 9
        $this->session->set($this->sessionName, $locale);
194 9
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
195 9
        if ($this->cookieDuration !== null) {
196 9
            $cookie = $cookie->withMaxAge($this->cookieDuration);
197
        }
198 9
        return $cookie->addToResponse($response);
199
    }
200
201 7
    private function parseLocale(string $locale): string
202
    {
203 7
        if (str_contains($locale, '-')) {
204 2
            [$locale] = explode('-', $locale, 2);
205 5
        } elseif (str_contains($locale, '_')) {
206 1
            [$locale] = explode('_', $locale, 2);
207
        }
208
209 7
        return $locale;
210
    }
211
212 6
    private function isRequestIgnored(ServerRequestInterface $request): bool
213
    {
214 6
        foreach ($this->ignoredRequests as $ignoredRequest) {
215 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
216 1
                return true;
217
            }
218
        }
219 5
        return false;
220
    }
221
222
    /**
223
     * @psalm-assert array<string, string> $this->locales
224
     *
225
     * @throws InvalidLocalesFormatException
226
     */
227 17
    private function assertLocalesFormat(): void
228
    {
229 17
        foreach ($this->locales as $code => $locale) {
230 17
            if (!is_string($code) || !is_string($locale)) {
231 1
                throw new InvalidLocalesFormatException();
232
            }
233
        }
234
    }
235
236 7
    private function getBaseUrl(): string
237
    {
238 7
        return rtrim($this->urlGenerator->getUriPrefix(), '/');
239
    }
240
241
    /**
242
     * @param array $locales List of supported locales in key-value format e.g. ['ru' => 'ru_RU', 'uz' => 'uz_UZ']
243
     *
244
     * @return $this
245
     */
246 1
    public function withLocales(array $locales): self
247
    {
248 1
        $new = clone $this;
249 1
        $new->locales = $locales;
250 1
        return $new;
251
    }
252
253 3
    public function withDefaultLocale(string $defaultLocale): self
254
    {
255 3
        $new = clone $this;
256 3
        $new->defaultLocale = $defaultLocale;
257 3
        return $new;
258
    }
259
260 1
    public function withQueryParameterName(string $queryParameterName): self
261
    {
262 1
        $new = clone $this;
263 1
        $new->queryParameterName = $queryParameterName;
264 1
        return $new;
265
    }
266
267 1
    public function withSessionName(string $sessionName): self
268
    {
269 1
        $new = clone $this;
270 1
        $new->sessionName = $sessionName;
271 1
        return $new;
272
    }
273
274 1
    public function withEnableSaveLocale(bool $enableSaveLocale): self
275
    {
276 1
        $new = clone $this;
277 1
        $new->enableSaveLocale = $enableSaveLocale;
278 1
        return $new;
279
    }
280
281 3
    public function withEnableDetectLocale(bool $enableDetectLocale): self
282
    {
283 3
        $new = clone $this;
284 3
        $new->enableDetectLocale = $enableDetectLocale;
285 3
        return $new;
286
    }
287
288
    /**
289
     * @param string[] $ignoredRequests
290
     *
291
     * @return $this
292
     */
293 2
    public function withIgnoredRequests(array $ignoredRequests): self
294
    {
295 2
        $new = clone $this;
296 2
        $new->ignoredRequests = $ignoredRequests;
297 2
        return $new;
298
    }
299
300 1
    public function withCookieSecure(bool $secure): self
301
    {
302 1
        $new = clone $this;
303 1
        $new->cookieSecure = $secure;
304 1
        return $new;
305
    }
306
}
307