Passed
Push — master ( cdd4ec...2189bc )
by Rustam
11:54 queued 08:52
created

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
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
c 1
b 0
f 0
nc 1
nop 10
dl 0
loc 13
ccs 2
cts 2
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\Aliases\Aliases;
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
final class Locale implements MiddlewareInterface
26
{
27
    private const DEFAULT_LOCALE = 'en';
28
    private const DEFAULT_LOCALE_NAME = '_language';
29
30
    private bool $enableSaveLocale = true;
31
    private bool $enableDetectLocale = false;
32
    private string $defaultLocale = self::DEFAULT_LOCALE;
33
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
34
    private string $sessionName = self::DEFAULT_LOCALE_NAME;
35
    private ?DateInterval $cookieDuration;
36
37
    /**
38
     * @param array $locales List of supported locales in key-value format e.g. ['ru' => 'ru_RU', 'uz' => 'uz_UZ']
39
     * @param string[] $ignoredRequests
40
     */
41 19
    public function __construct(
42
        private TranslatorInterface $translator,
43
        private UrlGeneratorInterface $urlGenerator,
44
        private SessionInterface $session,
45
        private Aliases $aliases,
46
        private LoggerInterface $logger,
47
        private ResponseFactoryInterface $responseFactory,
48
        private array $locales = [],
49
        private array $ignoredRequests = [],
50
        private bool $cookieSecure = false,
51
        private string $baseUrlAlias = '@baseUrl',
52
    ) {
53 19
        $this->cookieDuration = new DateInterval('P30D');
54
    }
55
56 18
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
57
    {
58 18
        if ($this->locales === []) {
59 1
            return $handler->handle($request);
60
        }
61
62 17
        $this->assertLocalesFormat();
63
64 16
        $uri = $request->getUri();
65 16
        $path = $uri->getPath();
66 16
        $query = $uri->getQuery();
67
68 16
        $locale = $this->getLocaleFromPath($path);
69
70 16
        if ($locale !== null) {
71 9
            $this->translator->setLocale($this->locales[$locale]);
72 9
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
73
74 9
            $response = $handler->handle($request);
75 9
            $newPath = null;
76 9
            if ($this->isDefaultLocale($locale) && $request->getMethod() === Method::GET) {
77 3
                $length = strlen($locale);
78 3
                $newPath = substr($path, $length + 1);
79
            }
80 9
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
81
        }
82 7
        if ($this->enableSaveLocale) {
83 7
            $locale = $this->getLocaleFromRequest($request);
84
        }
85 7
        if ($locale === null && $this->enableDetectLocale) {
86 2
            $locale = $this->detectLocale($request);
87
        }
88 7
        if ($locale === null || $this->isDefaultLocale($locale) || $this->isRequestIgnored($request)) {
89 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
90 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
91 2
            return $handler->handle($request);
92
        }
93
94 5
        $this->translator->setLocale($this->locales[$locale]);
95 5
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
96
97 5
        if ($request->getMethod() === Method::GET) {
98 4
            $location = rtrim($this->aliases->get($this->baseUrlAlias), '/') . '/'
99 4
                . $locale . $path . ($query !== '' ? '?' . $query : '');
100 4
            return $this->responseFactory
101 4
                ->createResponse(Status::FOUND)
102 4
                ->withHeader(Header::LOCATION, $location);
103
        }
104
105
106 1
        return $handler->handle($request);
107
    }
108
109 9
    private function applyLocaleFromPath(
110
        string $locale,
111
        ResponseInterface $response,
112
        string $query,
113
        ?string $newPath = null,
114
    ): ResponseInterface {
115 9
        if ($newPath === '') {
116 1
            $newPath = '/';
117
        }
118
119 9
        if ($newPath !== null) {
120 3
            $location = rtrim($this->aliases->get($this->baseUrlAlias), '/')
121 3
                . $newPath . ($query !== '' ? '?' . $query : '');
122 3
            $response = $this->responseFactory
123 3
                ->createResponse(Status::FOUND)
124 3
                ->withHeader(Header::LOCATION, $location);
125
        }
126 9
        if ($this->enableSaveLocale) {
127 9
            $response = $this->saveLocale($locale, $response);
128
        }
129 9
        return $response;
130
    }
131
132 16
    private function getLocaleFromPath(string $path): ?string
133
    {
134 16
        $parts = [];
135
        /**
136
         * @var string $code
137
         * @var string $locale
138
         */
139 16
        foreach ($this->locales as $code => $locale) {
140 16
            $parts[] = $code;
141 16
            $parts[] = $locale;
142
        }
143
144 16
        $pattern = implode('|', $parts);
145 16
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
146 9
            $matchedLocale = $matches[1];
147 9
            if (!isset($this->locales[$matchedLocale])) {
148 1
                $matchedLocale = $this->parseLocale($matchedLocale);
149
            }
150 9
            if (isset($this->locales[$matchedLocale])) {
151 9
                $this->logger->debug(sprintf("Locale '%s' found in URL", $matchedLocale));
152 9
                return $matchedLocale;
153
            }
154
        }
155 7
        return null;
156
    }
157
158 7
    private function getLocaleFromRequest(ServerRequestInterface $request): ?string
159
    {
160
        /** @var array<string, string> $cookies */
161 7
        $cookies = $request->getCookieParams();
162 7
        if (isset($cookies[$this->sessionName])) {
163 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
164 1
            return $this->parseLocale($cookies[$this->sessionName]);
165
        }
166
        /** @var array<string, string> $queryParameters */
167 6
        $queryParameters = $request->getQueryParams();
168 6
        if (isset($queryParameters[$this->queryParameterName])) {
169 4
            $this->logger->debug(
170 4
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
171 4
            );
172 4
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
173
        }
174 2
        return null;
175
    }
176
177 15
    private function isDefaultLocale(string $locale): bool
178
    {
179 15
        return $locale === $this->defaultLocale || $this->locales[$locale] === $this->defaultLocale;
180
    }
181
182 2
    private function detectLocale(ServerRequestInterface $request): ?string
183
    {
184 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
185 1
            if (!isset($this->locales[$language])) {
186 1
                $language = $this->parseLocale($language);
187
            }
188 1
            if (isset($this->locales[$language])) {
189 1
                return $language;
190
            }
191
        }
192 1
        return null;
193
    }
194
195 9
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
196
    {
197 9
        $this->logger->debug('Saving found locale to cookies');
198 9
        $this->session->set($this->sessionName, $locale);
199 9
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
200 9
        if ($this->cookieDuration !== null) {
201 9
            $cookie = $cookie->withMaxAge($this->cookieDuration);
202
        }
203 9
        return $cookie->addToResponse($response);
204
    }
205
206 7
    private function parseLocale(string $locale): string
207
    {
208 7
        if (str_contains($locale, '-')) {
209 2
            [$locale] = explode('-', $locale, 2);
210 5
        } elseif (str_contains($locale, '_')) {
211 1
            [$locale] = explode('_', $locale, 2);
212
        }
213
214 7
        return $locale;
215
    }
216
217 6
    private function isRequestIgnored(ServerRequestInterface $request): bool
218
    {
219 6
        foreach ($this->ignoredRequests as $ignoredRequest) {
220 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
221 1
                return true;
222
            }
223
        }
224 5
        return false;
225
    }
226
227
    /**
228
     * @psalm-assert array<string, string> $this->locales
229
     *
230
     * @throws InvalidLocalesFormatException
231
     */
232 17
    private function assertLocalesFormat(): void
233
    {
234 17
        foreach ($this->locales as $code => $locale) {
235 17
            if (!is_string($code) || !is_string($locale)) {
236 1
                throw new InvalidLocalesFormatException();
237
            }
238
        }
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 1
    public function withBaseUrlAlias(string $baseUrlAlias): self
308
    {
309 1
        $new = clone $this;
310 1
        $new->baseUrlAlias = $baseUrlAlias;
311 1
        return $new;
312
    }
313
}
314