Passed
Pull Request — master (#42)
by Rustam
03:25
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<array-key, string> $locales
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>.
Loading history...
39
     * @param string[] $ignoredRequests
40
     */
41 18
    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 18
        $this->cookieDuration = new DateInterval('P30D');
54
    }
55
56 17
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
57
    {
58 17
        if ($this->locales === []) {
59 1
            return $handler->handle($request);
60
        }
61
62 16
        $this->checkLocales();
63
64 15
        $uri = $request->getUri();
65 15
        $path = $uri->getPath();
66 15
        $query = $uri->getQuery();
67
68 15
        [$locale, $country] = $this->getLocaleFromPath($path);
69
70 15
        if ($locale !== null) {
71 8
            $this->translator->setLocale($locale);
72 8
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
73
74 8
            $response = $handler->handle($request);
75 8
            $newPath = null;
76 8
            if ($this->isDefaultLocale($locale, $country) && $request->getMethod() === Method::GET) {
77 3
                $length = strlen($locale);
78 3
                $newPath = substr($path, $length + 1);
79
            }
80 8
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
81
        }
82 7
        if ($this->enableSaveLocale) {
83 7
            [$locale, $country] = $this->getLocaleFromRequest($request);
84
        }
85 7
        if ($locale === null && $this->enableDetectLocale) {
86 2
            [$locale, $country] = $this->detectLocale($request);
87
        }
88 7
        if ($locale === null || $this->isDefaultLocale($locale, $country) || $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($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 8
    private function applyLocaleFromPath(
110
        string $locale,
111
        ResponseInterface $response,
112
        string $query,
113
        ?string $newPath = null,
114
    ): ResponseInterface {
115 8
        if ($newPath === '') {
116 1
            $newPath = '/';
117
        }
118
119 8
        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 8
        if ($this->enableSaveLocale) {
127 8
            $response = $this->saveLocale($locale, $response);
128
        }
129 8
        return $response;
130
    }
131
132
    /**
133
     * @return array{0:string|null, 1:string|null}
134
     */
135 15
    private function getLocaleFromPath(string $path): array
136
    {
137 15
        $parts = [];
138 15
        foreach ($this->locales as $code => $locale) {
139 15
            $parts[] = $code;
140 15
            $parts[] = $locale;
141
        }
142
143 15
        $pattern = implode('|', $parts);
144 15
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
145 8
            $matchedLocale = $matches[1];
146 8
            [$locale, $country] = $this->parseLocale($matchedLocale);
147 8
            if (isset($this->locales[$locale])) {
148 8
                $this->logger->debug(sprintf("Locale '%s' found in URL", $locale));
149 8
                return [$locale, $country];
150
            }
151
        }
152 7
        return [null, null];
153
    }
154
155
    /**
156
     * @return array{0:string|null, 1:string|null}
157
     */
158 7
    private function getLocaleFromRequest(ServerRequestInterface $request): array
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, null];
175
    }
176
177 14
    private function isDefaultLocale(string $locale, ?string $country): bool
0 ignored issues
show
Unused Code introduced by
The parameter $country is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

177
    private function isDefaultLocale(string $locale, /** @scrutinizer ignore-unused */ ?string $country): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
178
    {
179 14
        return $locale === $this->defaultLocale || $this->locales[$locale] === $this->defaultLocale;
180
    }
181
182
    /**
183
     * @return array{0:string|null, 1:string|null}
184
     */
185 2
    private function detectLocale(ServerRequestInterface $request): array
186
    {
187 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
188 1
            return $this->parseLocale($language);
189
        }
190 1
        return [null, null];
191
    }
192
193 8
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
194
    {
195 8
        $this->logger->debug('Saving found locale to cookies');
196 8
        $this->session->set($this->sessionName, $locale);
197 8
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
198 8
        if ($this->cookieDuration !== null) {
199 8
            $cookie = $cookie->withMaxAge($this->cookieDuration);
200
        }
201 8
        return $cookie->addToResponse($response);
202
    }
203
204
    /**
205
     * @return array{0:string, 1?:string|null}
206
     */
207 14
    private function parseLocale(string $locale): array
208
    {
209 14
        if (str_contains($locale, '-')) {
210 1
            return explode('-', $locale, 2);
211
        }
212
213 13
        if (str_contains($locale, '_')) {
214 1
            return explode('_', $locale, 2);
215
        }
216
217 12
        return [$locale, null];
218
    }
219
220 6
    private function isRequestIgnored(ServerRequestInterface $request): bool
221
    {
222 6
        foreach ($this->ignoredRequests as $ignoredRequest) {
223 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
224 1
                return true;
225
            }
226
        }
227 5
        return false;
228
    }
229
230
    /**
231
     * @throws InvalidLocalesFormatException
232
     */
233 16
    private function checkLocales(): void
234
    {
235 16
        foreach ($this->locales as $code => $locale) {
236 16
            if (!is_string($code) || !is_string($locale)) {
237 1
                throw new InvalidLocalesFormatException();
238
            }
239
        }
240
    }
241
242
    /**
243
     * @param array<array-key, string> $locales
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>.
Loading history...
244
     *
245
     * @return $this
246
     */
247 1
    public function withLocales(array $locales): self
248
    {
249 1
        $new = clone $this;
250 1
        $new->locales = $locales;
251 1
        return $new;
252
    }
253
254 3
    public function withDefaultLocale(string $defaultLocale): self
255
    {
256 3
        $new = clone $this;
257 3
        $new->defaultLocale = $defaultLocale;
258 3
        return $new;
259
    }
260
261 1
    public function withQueryParameterName(string $queryParameterName): self
262
    {
263 1
        $new = clone $this;
264 1
        $new->queryParameterName = $queryParameterName;
265 1
        return $new;
266
    }
267
268 1
    public function withSessionName(string $sessionName): self
269
    {
270 1
        $new = clone $this;
271 1
        $new->sessionName = $sessionName;
272 1
        return $new;
273
    }
274
275 1
    public function withEnableSaveLocale(bool $enableSaveLocale): self
276
    {
277 1
        $new = clone $this;
278 1
        $new->enableSaveLocale = $enableSaveLocale;
279 1
        return $new;
280
    }
281
282 3
    public function withEnableDetectLocale(bool $enableDetectLocale): self
283
    {
284 3
        $new = clone $this;
285 3
        $new->enableDetectLocale = $enableDetectLocale;
286 3
        return $new;
287
    }
288
289
    /**
290
     * @param string[] $ignoredRequests
291
     *
292
     * @return $this
293
     */
294 2
    public function withIgnoredRequests(array $ignoredRequests): self
295
    {
296 2
        $new = clone $this;
297 2
        $new->ignoredRequests = $ignoredRequests;
298 2
        return $new;
299
    }
300
301 1
    public function withCookieSecure(bool $secure): self
302
    {
303 1
        $new = clone $this;
304 1
        $new->cookieSecure = $secure;
305 1
        return $new;
306
    }
307
308 1
    public function withBaseUrlAlias(string $baseUrlAlias): self
309
    {
310 1
        $new = clone $this;
311 1
        $new->baseUrlAlias = $baseUrlAlias;
312 1
        return $new;
313
    }
314
}
315